<?php

/**
 * MNet Library 
 * Copyright (C) 2006-2008 Catalyst IT Ltd (http://www.catalyst.net.nz)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @package    mnet
 * @subpackage core
 * @author     Catalyst IT Ltd
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
 * @copyright  (C) 2006-2008 Catalyst IT Ltd http://catalyst.net.nz
 * @copyright  (C) portions from Moodle, (C) Martin Dougiamas http://dougiamas.com
 *
 */


define('MNET_LOG_ERR', 'err');
define('MNET_LOG_DEBUG', 'debug');
define('MNET_LOG_WARN', 'warn');
define('MNET_LOG_NOTICE', 'notice');

/**
 * This class must be extended and implemented by any application that wishes
 * to make use of MNet.
 * 
 * It provides the basic set of functions that MNet needs to operate,
 * abstracting these functions away from the application that MNet is running
 * on top of
 */
abstract class mnet_application {
    /**
     * Return the wwwroot of the current host. This is of the form
     * http://domain/path/to/webroot without trailing slash. The MNet
     * scripts for your application type are expected to be under this
     * root.
     * 
     * @return string the webroot
     */
    public abstract function get_local_wwwroot();

    /**
     * Submit an HTTP POST request.
     * 
     * Any proxies (HTTP or SOCKS5) in use locally must be used if configured.
     *
     * @param string $url the URL to send to
     * @param array $headers an associative array of HTTP headers to send with the POST
     * @param array $postdata an associative array of POST values to send
     * @param int $timeout an optional timeout value in seconds
     * @return array an associative array with parameters:
     *         'response': the response text
     *         'code'    : the HTTP response code from the remote server
     */
    public function http_post($url, $headers, $postdata, $timeout = null) {
        // Provide a base implementation using CURL
        // Initialize request the target URL
        $curl_headers = array();
        foreach ($headers as $k => $v) {
            $curl_headers[] = $k.': '.$v;
        }
        $httprequest = curl_init($url);
        if ($timeout) {
            curl_setopt($httprequest, CURLOPT_TIMEOUT, $timeout);
        }
        curl_setopt($httprequest, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($httprequest, CURLOPT_POST, true);
        curl_setopt($httprequest, CURLOPT_USERAGENT, 'MNet');
        curl_setopt($httprequest, CURLOPT_HTTPHEADER, $curl_headers);
        curl_setopt($httprequest, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($httprequest, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($httprequest, CURLOPT_POSTFIELDS, $postdata);

        $response = array(
            'response' => curl_exec($httprequest),
            'code'     => curl_getinfo($httprequest, CURLINFO_HTTP_CODE),
            'error'    => curl_error($httprequest)
        );

        curl_close($httprequest);
        return $response;
    }

    /**
     * Log a function using the native application's logging API.
     * 
     * @param string $type one of the MNET_LOG_* constants
     * @param string $message the message to log
     */
    public abstract function log($type, $message);

    /**
     * Save an mnet_peer record somewhere (usually a database table prepared for it)
     * 
     * The id field of $peer should be modified by reference once saved, if the record
     * is new.
     *
     * @param mnet_peer $host the peer record to save
     */
    public abstract function save_peer(mnet_peer &$peer);

    /**
     * Fetch an mnet_peer record (usually from a database) by its www-root (primary key)
     * 
     * @param string $wwwroot the HTTP document root of the remote system
     * @return mnet_peer an mnet_peer or mnet_remote_client (depending on $class) record if found, null otherwise
     */
    public abstract function get_peer_by_wwwroot($wwwroot);

    /**
     * Returns true if we consider the passed peer to be reliable.
     * This is called when a remote client calls in, but their request isn't
     * signed/encrypted properly. If the host is trusted, we'll attempt to
     * keyswap with them. Checking the peer record's IP against a whitelist
     * is a common way to implement this
     *
     * @param mnet_peer $peer the remote client peer to check
     * @return bool true if we trust this client and can proceed with communications
     */
    public abstract function peer_is_trusted(mnet_peer $peer);

    /**
     * Returns the currently configured certificate and PEM strings from
     * storage, or null if none stored
     *
     * return array associative array with keys 'certificate' and 'keypair_PEM'
     */
    public abstract function get_current_keypair();

    /**
     * Returns a history of previous keypairs used on this site, excluding
     * the current one.
     * 
     * @return array an array of associative arrays, each containing 'certificate' and 'keypair_PEM' keys
     */
    public abstract function get_keypair_history();

    /**
     * Store a new local keypair in the history, also rotating out any past
     * entries that have become too old. This new keypair should be returned
     * by subsequent calls to get_configured_keypair()
     * 
     * @param string $certificate the new certificate string
     * @param string $keypair_pem the new keypair_pem
     */
    public abstract function set_new_keypair($certificate, $keypair_pem);

    /**
     * Returns the ISO 2-letter country code for the server. Used for generating
     * certificate keypairs.
     *
     * @return string ISO 2-letter country code, in uppercase
     */
    public function get_country() {
        return 'NZ';
    }

    /**
     * Return the locality of the server. Used for generating certificate keypairs.
     * 
     * return string the name of the city etc that the server is located in
     */
    public function get_locality() {
        return 'Wellington';
    }

    /**
     * Returns the organisation managing this site. Used for generating certificate
     * keypairs.
     * 
     * @return string the organisation name used to sign certificates
     */
    public function get_organisation_name() {
        return $this->get_local_wwwroot();
    }

    /**
     * Return the email address to attach to the certificate
     *
     * @return string an email address
     */
    public function get_certificate_email() {
        return 'mnet@example.org';
    }

    /**
     * Return the content of a URL as a string. If the request fails, return
     * null instead.
     *
     * @param string $url the absolute URL to download
     * @return string the content from the URL, or null on failure
     */
    public function get_url_content($url) {
        // will only work with url_fopen enabled
        return file_get_contents($url);
    }

    /**
     * Returns the absolute URL to the handler that will serve up the OpenSSL
     * public key X.509 certificate
     * 
     * The page at that URL may do this by calling $MNET->get_public_key_cert()
     * and outputting the result.
     *
     * @return string absolute URL to public key retrieval, for signing
     */
    public abstract function get_public_key_url();
    
    /**
     * Return the path (after our local wwwroot) to the location accepting
     * incoming MNet requests for the current application, eg. /mnet/server.php
     * 
     * This location will need to call mnet_server::handle_request();
     * 
     * @return string the path, eg. /mnet/server.php
     */
    public abstract function get_server_path();

    /**
     * Cleans a base64 string, ensuring there's nothing invalid in it
     * 
     * This is from Moodle's clean_param($param, PARAM_BASE64);
     * 
     * Moodle's implementation should override this and forward it to the real clean_param()
     * 
     * @param string $param the base64-encoded string
     * @return string the cleaned base64 string
     */
    public function clean_base64($param) {
        if (!empty($param)) {
            // PEM formatted strings may contain letters/numbers and the symbols
            // forward slash: /
            // plus sign:     +
            // equal sign:    =
            if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
                return '';
            }
            $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
            // Each line of base64 encoded data must be 64 characters in
            // length, except for the last line which may be less than (or
            // equal to) 64 characters long.
            for ($i=0, $j=count($lines); $i < $j; $i++) {
                if ($i + 1 == $j) {
                    if (64 < strlen($lines[$i])) {
                        return '';
                    }
                    continue;
                }
    
                if (64 != strlen($lines[$i])) {
                    return '';
                }
            }
            return implode("\n",$lines);
        } else {
            return '';
        }
    }

    /**
     * Cleans a PEM certificate and makes sure there's nothing invalid in it
     *
     * This is from moodle's clean_param($param, PARAM_PEM);
     * 
     * Moodle's implementation should override this and forward it to the real clean_param()
     * 
     * @param string $param the PEM certificate
     * @return string the cleaned certificate
     */
    public function clean_pem($param) {
        $param = trim($param);
        // PEM formatted strings may contain letters/numbers and the symbols
        // forward slash: /
        // plus sign:     +
        // equal sign:    =
        // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes
        if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
            list($wholething, $body) = $matches;
            unset($wholething, $matches);
            $b64 = $this->clean_base64($body);
            if (!empty($b64)) {
                return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
            } else {
                return '';
            }
        }
        return '';
    }

    // CONFIG ITEMS

    
    /**
     * Returns how much time drift between client and server to allow
     *
     * We default to 15 seconds
     * 
     * @return int the number of seconds drift between peers that is allowed
     */
    public function get_drift_threshold() {
        return 15;
    }

    /**
     * Whether or not to register all new hosts as we meet them.
     * If set false, hosts will need to be registered in advance
     *
     * @return bool true if we're allowed to automatically register new hosts and their pubkeys
     */ 
    public function should_register_hosts() {
        return true;
    }

    /**
     * Look up an MNet 1.0 application ID and translate it into a path 
     *
     * @param string $app the app, one of 'moodle', 'mahara', 'drupal'
     * @return string the path to its XML-RPC server
     */
    public static function get_path_for_app($app) {
        $mappings = self::get_app_mappings();
        return $mappings[$app];
    }

    /**
     * Look up the MNet 1.0 application ID for a given XML-RPC server path
     * 
     * @param string $path the path, eg. /mnet/xmlrpc/server.php for Moodle
     * @return string the applicationid
     */
    public static function get_appid_for_path($path) {
        $mappings = self::get_app_mappings();
        return array_search($path, $mappings);
    }

    /**
     * Return the lookup tables between MNet`1.0 application IDs and paths
     * @return array assoc array of app IDs to paths
     */
    private static function get_app_mappings() {
        return array(
            'drupal' => '/mnet/server',
            'moodle' => '/mnet/xmlrpc/server.php',
            'mahara' => '/api/xmlrpc/server.php'
        );
    }
}
